Animation State Switching
Learn how to switch between different animations based on player input and game state.
Overview
This tutorial shows you how to:
- Create a state-based animation system
- Switch between idle, running, and jumping animations
- Trigger animations based on input
Prerequisites
This tutorial assumes you have:
- Completed the Play Animation tutorial
- A project with multiple animations loaded (idle, running, jumping)
- An entity with a skeleton that supports animation
For complete API documentation:
Step 1: Set Up Animation States
Define an enum to represent your animation states:
enum PlayerState {
case idle
case running
case jumping
}
class GameScene {
var player: EntityID!
var playerState: PlayerState = .idle
init() {
// ... setup code ...
startGameSystems()
// Register input
InputSystem.shared.registerKeyboardEvents()
player = findEntity(name: "Player")
// Load all animations -- ignore if you linked all three animations through the editor.
loadPlayerAnimations()
// Start with idle
changeAnimation(entityId: player, name: "idle")
}
func loadPlayerAnimations() {
setEntityAnimations(
entityId: player,
filename: "idle",
withExtension: "usdc",
name: "idle"
)
setEntityAnimations(
entityId: player,
filename: "running",
withExtension: "usdc",
name: "running"
)
setEntityAnimations(
entityId: player,
filename: "jumping",
withExtension: "usdc",
name: "jumping"
)
}
}
Step 2: Implement State Switching Logic
Create a function to handle state transitions:
class GameScene {
var player: EntityID!
var playerState: PlayerState = .idle
var isGrounded: Bool = true // Track if player is on ground
func update(deltaTime: Float) {
if gameMode == false { return }
updatePlayerState()
}
func updatePlayerState() {
let oldState = playerState
// Determine new state based on input and game conditions
if !isGrounded {
playerState = .jumping
} else if isMovementKeyPressed() {
playerState = .running
} else {
playerState = .idle
}
// Only change animation if state actually changed
if playerState != oldState {
switchToAnimation(for: playerState)
}
}
func isMovementKeyPressed() -> Bool {
return inputSystem.keyState.wPressed ||
inputSystem.keyState.aPressed ||
inputSystem.keyState.sPressed ||
inputSystem.keyState.dPressed
}
func switchToAnimation(for state: PlayerState) {
switch state {
case .idle:
changeAnimation(entityId: player, name: "idle")
Logger.log(message: "Switched to idle animation")
case .running:
changeAnimation(entityId: player, name: "running")
Logger.log(message: "Switched to running animation")
case .jumping:
changeAnimation(entityId: player, name: "jumping")
Logger.log(message: "Switched to jumping animation")
}
}
}
Step 3: Add Jump Trigger
Add space bar input to trigger jumping:
class GameScene {
var player: EntityID!
var playerState: PlayerState = .idle
var isGrounded: Bool = true
var jumpTimer: Float = 0.0
let jumpDuration: Float = 0.5 // Jump animation duration in seconds
func update(deltaTime: Float) {
if gameMode == false { return }
handleJumpInput()
updateJumpTimer(deltaTime: deltaTime)
updatePlayerState()
}
func handleJumpInput() {
// Trigger jump on space press (only if grounded)
if inputSystem.keyState.spacePressed && isGrounded {
isGrounded = false
jumpTimer = jumpDuration
}
}
func updateJumpTimer(deltaTime: Float) {
// Count down jump timer
if !isGrounded {
jumpTimer -= deltaTime
// Land when timer expires
if jumpTimer <= 0.0 {
isGrounded = true
jumpTimer = 0.0
}
}
}
func updatePlayerState() {
let oldState = playerState
// Priority: jumping > running > idle
if !isGrounded {
playerState = .jumping
} else if isMovementKeyPressed() {
playerState = .running
} else {
playerState = .idle
}
if playerState != oldState {
switchToAnimation(for: playerState)
}
}
}
Step 4: Combine Animation with Movement
Integrate animation state switching with actual movement:
class GameScene {
var player: EntityID!
var playerState: PlayerState = .idle
var isGrounded: Bool = true
var jumpTimer: Float = 0.0
let moveSpeed: Float = 5.0
let jumpDuration: Float = 0.5
func update(deltaTime: Float) {
if gameMode == false { return }
handleJumpInput()
updateJumpTimer(deltaTime: deltaTime)
updatePlayerState()
updatePlayerMovement(deltaTime: deltaTime)
}
func updatePlayerMovement(deltaTime: Float) {
var movement = SIMD3<Float>(0, 0, 0)
// Only move if grounded
if isGrounded {
if inputSystem.keyState.wPressed {
movement.z += moveSpeed * deltaTime
}
if inputSystem.keyState.sPressed {
movement.z -= moveSpeed * deltaTime
}
if inputSystem.keyState.aPressed {
movement.x -= moveSpeed * deltaTime
}
if inputSystem.keyState.dPressed {
movement.x += moveSpeed * deltaTime
}
if movement != SIMD3<Float>(0, 0, 0) {
translateBy(entityId: player, delta: movement)
}
}
}
}
Advanced: State Machine Pattern
For more complex state management, consider using a proper state machine:
class AnimationStateMachine {
var currentState: PlayerState = .idle
var entityId: EntityID
init(entityId: EntityID) {
self.entityId = entityId
}
func canTransition(to newState: PlayerState) -> Bool {
switch (currentState, newState) {
case (.jumping, .idle), (.jumping, .running):
// Can only exit jumping state when landing
return false
default:
return true
}
}
func transition(to newState: PlayerState) {
guard canTransition(to: newState) else { return }
if currentState != newState {
currentState = newState
playAnimation(for: newState)
}
}
func playAnimation(for state: PlayerState) {
let animationName: String
switch state {
case .idle: animationName = "idle"
case .running: animationName = "running"
case .jumping: animationName = "jumping"
}
changeAnimation(entityId: entityId, name: animationName)
}
}
Summary
You've learned:
✅ Create animation states using enums
✅ Switch animations based on game state
✅ Trigger animations from input
✅ Prevent animation flickering with state checks
✅ Combine animations with movement logic